
package Middleware;

import java.io.BufferedWriter;
import java.io.ByteArrayOutputStream;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.OutputStreamWriter;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.math.BigInteger;
import java.net.InetAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.security.KeyFactory;
import java.security.PublicKey;
import java.security.Signature;
import java.security.spec.RSAPublicKeySpec;
import java.util.Calendar;

import javax.crypto.spec.SecretKeySpec;
import javax.smartcardio.CommandAPDU;
import javax.smartcardio.ResponseAPDU;
import javax.swing.JOptionPane;
import javax.swing.JPasswordField;

import be.msec.client.connection.IConnection;
import be.msec.client.connection.SimulatedConnection;
import be.msec.client.utils.TextOperations;


public class Middleware implements Runnable {
	private final static byte IDENTITY_CARD_CLA =(byte)0x80;
	private static final byte VALIDATE_PIN_INS = 0x22;
	private static final byte GET_LASTVALIDATE_TIME_INS = 0x23;
	private static final byte GET_IDENTITY_INS = 0x27;
	private final static short SW_VERIFICATION_FAILED = 0x6300;
	private final static short SW_PIN_VERIFICATION_REQUIRED = 0x6301;
    private static final byte GET_SERIAL_INS = 0x26;
    private static final byte GET_NAME_INS = 0x24;
    private final static byte GET_SIGN_INS =0x28;
    private final static byte GET_CERT_INS =0x29;
    private final static byte GET_ATTEMPTS_INS =0x30;
    private static final byte AUTH_SP_INS = 0x25;
    private final static byte PUT_TIME = 0x31;
	private final static byte PUT_TIME_SIGNATURE = 0x32;
	private static final byte CA_CERT_TEST = 0x35;
	private static final byte CHECK_CHALLENGE = 0x36;
	private static final byte AUTH_CARD = 0x37;
	private static final byte GET_ALL_ATTRIBUTES = 0x38;
	private static final byte GET_TIME_REVALIDATION_REQUIRED = 0x39;
	
	private static final byte SP_CONNECT = 0x01;
	private static final byte SP_AUTH_SP = 0x02;
	private static final byte SP_AUTH_CARD = 0x03;
	private static final byte SP_ATTR = 0x04;
	private static final byte SP_CHALLENGE_PLUS_ONE = 0x05;
	private static final byte SP_BREAK = 0x06;
	
	private static final byte SP_OK = 0x10;
	private static final byte SP_NK = 0x11;
	
	private static IConnection c;
	private static CommandAPDU a;
	private static ResponseAPDU r;
	
	
	//SP
	private static SecretKeySpec symetricKeyFromCard;
	private Method logMethod;
	private MiddlewareGUI guiInstance;
	
	public Middleware(Method logMethod, MiddlewareGUI guiInstance) {
		this.logMethod = logMethod;
		this.guiInstance = guiInstance;
	}
	
	public void log(String s) {
		try {
			logMethod.invoke(this.guiInstance, s + "\n");
		} catch (IllegalAccessException e) {
			e.printStackTrace();
		} catch (IllegalArgumentException e) {
			e.printStackTrace();
		} catch (InvocationTargetException e) {
			e.printStackTrace();
		}
	}

	public void run() {
		   
		try 
		  {
		     /* Server opzetten */
			 ServerSocket listensocket=new ServerSocket(2222);
			 while(true) {
				 log("Waiting for client");
				 Socket client=listensocket.accept();
				 log("Client accepted");
				 log(" \n");
				 DataInputStream dis = new DataInputStream(client.getInputStream());
			     DataOutputStream dos = new DataOutputStream(client.getOutputStream());
				 /* Wachten op input */
			     while(client.getInputStream().available() == 0 ) {
			    	 Thread.sleep(100);
			     }
			     boolean stayActive = true;
			     while(stayActive) {
				     /* Starten met Lezen */
				     byte command = dis.readByte();
				     log("Read Command");
				     
				     switch(command) {
				     	case SP_CONNECT: 
	     					log("- Connect Command");
	     					boolean succes = InitializeCard();
     						dos.writeBoolean(succes);
     						if (succes == false) {
     							log("-Force break connection Command");
    				     		stayActive = false;
     						}
     						log(" \n");
	     					break;
				     	case SP_AUTH_SP: 
				     		log("- Authenticate SP Command");
				     		short certLength = dis.readShort();
				     		byte[] cert = new byte[certLength];
				     		dis.read(cert, 0, certLength);
				     		byte[] data = authenticateServiceProvider(cert);
				     		dos.writeShort(data.length);
				     		dos.write(data);
				     		log("\n");
				     		break;
				     	case SP_CHALLENGE_PLUS_ONE:
				     		log("-Return challenge Command");
				     		short challengeLength = dis.readShort();
				     		byte[] challenge = new byte[challengeLength];
				     		dis.read(challenge, 0, challengeLength);
				     		dos.writeBoolean(SendChallengePlusOne(challenge));
				     		log(" \n");
				     		break;
				     	case SP_AUTH_CARD:
				     		log("-Authenticate Card Command");
				     		challengeLength = dis.readShort();
				     		challenge = new byte[challengeLength];
				     		dis.read(challenge, 0, challengeLength);
				     		byte[] response = authenticateCard(challenge);
				     		dos.writeShort(response.length);
				     		dos.write(response);
				     		log(" \n");
				     		break;
				     	case SP_ATTR:
				     		 log("-Get Attributes Command");
				     		 byte[] attributes_encrypted = releaseAttributes();
				     		 dos.writeShort(attributes_encrypted.length);
				     		 dos.write(attributes_encrypted);
				     		 log(" \n");
			     			 break;
				     	case SP_BREAK:
				     		log("-Get break connection Command");
				     		stayActive = false;
				     		c.close();
				     		break;
				     }
			     }
			     client.close();
			  }	//end while(true)
		  }	//end try{...
		  catch (Exception e)
		  {
		     log(e.getMessage());
		  }
	}
	
	 private boolean InitializeCard() {
		 try{
			 c = null;
			 
			 /* Connectie met kaart starten */
			 c = new SimulatedConnection();
			 c.connect();
			 a = new CommandAPDU(0x00, 0xa4, 0x04, 0x00,new byte[]{0x01,0x02,0x03,0x04, 0x05, 0x06, 0x07, 0x08, 0x09,0x00, 0x00 });
			 r = c.transmit(a);
			 if (r.getSW()!=0x9000) throw new Exception("Applet selection failed");	
			
			 /*** Stap 1 ***/
			 if(timeRevalidationRequired(c, a, r)) {	
				 log("-- Time revalidation needed! Requesting time from Government server...");
				 if(!getTimeFromServerAndForwardToCard(c,a, r)){
					 //Indien er een fout is opgetreden met het doorsturen van de tijd naar de kaart
					 log("-- Error! Card requested new time validation but could not send the government time to the card!");				 
					 return false;
				 } 
			 }
		 }catch(Exception ex) {
			 log("-- Something went wrong: " + ex.getMessage());
			 return false;
		 }
		 return true;
	}
	 
	 private boolean timeRevalidationRequired(IConnection c, CommandAPDU a, ResponseAPDU r) throws Exception {
		ByteArrayOutputStream bos = new ByteArrayOutputStream();
		DataOutputStream out = new DataOutputStream(bos);
		
		Calendar cal = Calendar.getInstance();
		
		out.writeShort(cal.get(Calendar.YEAR));
		out.writeByte(cal.get(Calendar.MONTH));
		out.writeByte(cal.get(Calendar.DAY_OF_MONTH));
		 
		log("-- Sending date");
		a = new CommandAPDU(IDENTITY_CARD_CLA, GET_TIME_REVALIDATION_REQUIRED, 0x00, 0x00, bos.toByteArray());
		r = c.transmit(a);
		 
		byte[] input = r.getData();
		if(input[0] == 'y') {
			log("-- Time to card was a succes");
			return true;
		}
		log("-- Time to card was not a succes");
		return false;
	}

	public byte[] releaseAttributes() throws Exception{
		//SP: pin ingeven
		//0x00 = naam
		//0x01 = adres
		//0x02 = land
		//0x03 = geboortedatum
		//0x04 = leeftijd
		//0x05 = geslacht
		//0xff = error
		//100 = einde van de attributen
		JPasswordField pf = new JPasswordField();
		int okCxl = JOptionPane.showConfirmDialog(null, pf, "Enter Password", JOptionPane.OK_CANCEL_OPTION, JOptionPane.PLAIN_MESSAGE);
		String password = null;
		if (okCxl == JOptionPane.OK_OPTION) {
		  password = new String(pf.getPassword());
		  System.err.println("You entered: " + password);
		}
		ByteArrayOutputStream bos = new ByteArrayOutputStream();
		DataOutputStream out = new DataOutputStream(bos);
		out.writeByte(Integer.parseInt(password.charAt(0)+""));
		out.writeByte(Integer.parseInt(password.charAt(1)+""));
		out.writeByte(Integer.parseInt(password.charAt(2)+""));
		out.writeByte(Integer.parseInt(password.charAt(3)+""));		
		
		log("-- Getting all attributes");
		a = new CommandAPDU(IDENTITY_CARD_CLA, GET_ALL_ATTRIBUTES, 0x00, 0x00, bos.toByteArray());
		r = c.transmit(a);
		return r.getData();
		
		
	}
	 
	 public byte[] authenticateCard(byte[] challenge) throws Exception {
		 a = new CommandAPDU(IDENTITY_CARD_CLA, AUTH_CARD, 0x00, 0x00, challenge);
		 r = c.transmit(a);
		 log("-- Recieving signature and certificate");
		 return r.getData();

	 }
	 
	 public byte[] authenticateServiceProvider(byte[] certificate) throws Exception {  
		 a = new CommandAPDU(IDENTITY_CARD_CLA, AUTH_SP_INS, 0x00, 0x00, certificate);
		 r = c.transmit(a);
		 log("-- Recieving key and challenge");
		 return r.getData();
		
	 }
	 
	 public boolean SendChallengePlusOne(byte[] data) throws Exception {
		 log("-- Sending new incrmented challenge");
		 a = new CommandAPDU(IDENTITY_CARD_CLA, CHECK_CHALLENGE, 0x00, 0x00, data);
		 r = c.transmit(a);
		 
		 byte[] input = r.getData();
		 log("-- Authenticate SP on card: " + new String(input));
		 String status = new String(input);
		 if(status.equalsIgnoreCase("Ok")) {
			 return true;
		 }
		 return false;
	 }
	 
	 public boolean getTimeFromServerAndForwardToCard(IConnection c, CommandAPDU a, ResponseAPDU r) {
		try {
		     /* Config*/
		     InetAddress serveraddress=InetAddress.getByName("127.0.0.1");
		     Socket server=new Socket(serveraddress,1234);
		     log("-- Connected to time-server");
		     BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(server.getOutputStream()));
		     log("-- Requesting revalidation. Connected: " + server.isConnected());
		     
		     /* Begin Send */
		     writer.write("RevalidationRequest");
		     writer.newLine();
		     writer.flush();
		     log("-- Reading time");
		     
		     /* Wachten op data input */
		     while(server.getInputStream().available() == 0 ) {
		    	 Thread.sleep(500);
		     }
		     
		     /* Data inlezen */
		     byte[] data = new byte[256];
		     server.getInputStream().read(data);
		     
		     /* Tijd ophalen */
		     byte[] dateBytes = getTimeFromByteArray(data);

		     /* Signature ophalen */
		     byte[] signatureBytes = getSignatureFromByteArray(data);			    			     
		     
		     //!!!!!!!!!!!!!!!!!!!!!!!!OPMERKING!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
		     //Om te verifyen in Middleware moet deze nul =========\/ er staan! Op de kaart niet!!!!!!!!!!!
		     byte[] government_publicKey_modulus = new byte[]{(byte) 0,(byte) -44,(byte) 127,(byte) 42,(byte) 97,(byte) -72,(byte) 91,(byte) -37,(byte) 98,(byte) 3,(byte) 105,(byte) -92,(byte) 63,(byte) 57,(byte) -117,(byte) -78,(byte) -7,(byte) -48,(byte) 41,(byte) -110,(byte) -101,(byte) -59,(byte) 56,(byte) 105,(byte) 29,(byte) -125,(byte) -3,(byte) 53,(byte) -50,(byte) 50,(byte) -7,(byte) 25,(byte) 60,(byte) -79,(byte) -71,(byte) 101,(byte) -75,(byte) 72,(byte) 72,(byte) -46,(byte) 16,(byte) -53,(byte) 122,(byte) -14,(byte) -112,(byte) -10,(byte) -24,(byte) 111,(byte) -9,(byte) 85,(byte) -71,(byte) -86,(byte) -8,(byte) -47,(byte) -125,(byte) 71,(byte) 19,(byte) -106,(byte) 30,(byte) 105,(byte) -34,(byte) 72,(byte) 29,(byte) 117,(byte) -53};
		     byte[] government_publicKey_exponent = new byte[] {(byte) 1,(byte) 0,(byte) 1};
		     BigInteger bi_modulus = new BigInteger(government_publicKey_modulus);
		     BigInteger bi_exponent = new BigInteger(government_publicKey_exponent);
		     
			 RSAPublicKeySpec spec = new RSAPublicKeySpec(bi_modulus, bi_exponent);
			 KeyFactory factory = KeyFactory.getInstance("RSA");
			 PublicKey publicKey = factory.generatePublic(spec);
			 Signature verifier = Signature.getInstance("SHA1withRSA");
			 verifier.initVerify(publicKey);
			 verifier.update(dateBytes);
			 if(verifier.verify(signatureBytes)) {
				 log("-- Verify in Middleware is Ok");
			 }
			 else{
				 log("-- Woooops, verify in Middleware is Nok!");
			 }
			 
			 //Tijd + signature doorsturen naar de kaart
		     forwardValidationTimeToCard(dateBytes, signatureBytes, c, a, r);
	     
		     /* close */
		     server.close();
		  }
		catch (Exception e)
		  {
		     log(e.getMessage());
		     return false;
		  }
		return true;
	 }
	 
	 private void forwardValidationTimeToCard(byte[] dateBytes, byte[] signatureBytes,IConnection c, CommandAPDU a, ResponseAPDU r) throws Exception {
		 //Doorsturen tijd (dateBytes)
		 a = new CommandAPDU(IDENTITY_CARD_CLA, PUT_TIME, 0x00, 0x00, dateBytes);
		 r = c.transmit(a);
		 String data = TextOperations.getString(r.getData(), 0, r.getData().length); // Inlezen "Ok" antwoord vd kaart
		 log("-- Tijd ontvangen op de kaart: " + data);

		 //Doorsturen signature (signatureBytes)	 
		 a = new CommandAPDU(IDENTITY_CARD_CLA, PUT_TIME_SIGNATURE, 0x00, 0x00, signatureBytes);
		 r = c.transmit(a);
		 data = TextOperations.getString(r.getData(), 0, r.getData().length); // Inlezen antwoord (Ok of Nok)
		 log("-- Verificatie op de kaart: " + data);
	 }
	 
	 public byte[] getSignatureFromByteArray(byte[] data) {
		 String s = new String(data);
	     String[] values = s.split("-_@@_-");
	     int lengthTime = Integer.parseInt(values[1]);
	     int lengthSig = Integer.parseInt(values[2]);
	     
	     byte[] signature = new byte[lengthSig];
	     System.arraycopy(data, lengthTime, signature, 0, lengthSig);
		 return signature;
	 }
	 
	 public byte[] getTimeFromByteArray(byte[] data) {
		String s = new String(data);
	    String[] values = s.split("-_@@_-");
	    int lengthTime = Integer.parseInt(values[1]);
	     
	    byte[] timeData = new byte[lengthTime]; //arraycopy(Object src, int srcPos, Object dest, int destPos, int length) 
	    System.arraycopy(data, 0, timeData, 0, lengthTime);
		return timeData;
	 }
	 
	 public byte[] readFile(String filename) {
		File file = new File(filename);
		byte[] fileContent = null;
		try {
			FileInputStream myCert = new FileInputStream(filename);
			fileContent = new byte[(int)file.length()];
			myCert.read(fileContent);
		} catch (Exception e) {
			e.printStackTrace();
		}
		return fileContent;
	}
}
